package zpe.genericcrud.jsf.view.bean;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.faces.bean.ManagedProperty;
import javax.faces.event.ActionEvent;
import javax.faces.event.ValueChangeEvent;
import javax.faces.model.DataModel;
import javax.faces.model.ListDataModel;
import javax.faces.model.SelectItem;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import zpe.genericcrud.exception.ApplicationException;
import zpe.genericcrud.model.Entidade;
import zpe.genericcrud.service.ServiceCrud;
import zpe.genericcrud.util.PropertiesUtils;
import zpe.genericcrud.util.ReflectionUtil;

import static zpe.genericcrud.jsf.view.bean.FacesUtils.*;

/**
 * Superclasse para os managed beans. Oferece os m�todos b�sicos para
 * manipula��o de uma entidade e manuten��o de estados de uma p�gina.
 * 
 * @author Elias Sales, Samuel Soares
 * @since 29/05/2009
 * @param <Entity>
 *            Entidade que ser� manipulada.
 * @param <Service>
 *            Servi�o que manipula a entidade.
 */
public abstract class AbstractCrudBean<Entity extends Entidade, Service extends ServiceCrud<Entity>> {
	// TODO colocar logs
	protected static Log logger = LogFactory.getLog("");

	/** Local onde fica o arquivo properties de mensagens. */
	private String fileMessages = "/zpe/genericcrud/jsf/view/bean/MessagesResources.properties";

	/**
	 * Properties de mensagens, carregado com o arquivo configurado em
	 * fileMessages.
	 */
	private Properties messageProperties;

	/**
	 * Mant�m a p�gina no estado de edi��o.
	 */
	public static final String STATE_EDIT = "edit";

	/**
	 * Mant�m a p�gina no estado de inser��o.
	 */
	public static final String STATE_INSERT = "insert";

	/**
	 * Mant�m a p�gina no estado de exclus�o.
	 */
	public static final String STATE_DELETE = "delete";

	/**
	 * Mant�m a p�gina no estado de visualiza��o.
	 */
	public static final String STATE_VIEW = "view";

	/**
	 * Mant�m a p�gina no estado de consulta. Esse � o estado padr�o.
	 */
	public static final String STATE_SEARCH = "search";

	/**
	 * Atributo que indica qual o estado atual da p�gina.
	 */
	private String currentState;

	/**
	 * Entidade de cadastro e atualiza��o.
	 */
	private Entity entity;

	/**
	 * Entidade de consulta. Necess�rio para manter os dados da consulta
	 * quando alternando entre estados.
	 */
	private Entity entitySearch;

	/**
	 * Entidade que pode ser utilizada para referenciar um objeto selecionado de
	 * uma lista.
	 */
	private Entity selectEntity;

	/**
	 * Op��o de filtro, sobre uma lista consultada, selecionada pelo
	 * usu�rio.
	 */
	// private String letraPesquisa;

	/**
	 * HtmlDatascroller da lista principal do estado de consulta. S� pode ser
	 * utilizado num �nico dataTable. Para utilizar em outro dataTable, deve
	 * ser criado outro objeto HtmlDatascroller.
	 */
	// private HtmlDatascroller scroller = new HtmlDatascroller(); //TODO
	// pendente implementar nova vers�o para o dataScroller.

	/**
	 * Mant�m o n�mero da p�gina do HtmlDatascroller principal. Tem que
	 * ter um valor para poder funcionar na primeira vez.
	 */
	private Integer numeroPagina = 1;

	/**
	 * Lista entidades de uma consulta.
	 */
	private DataModel<Entity> resultSearch;

	/**
	 * Lista de entidades que ser�o inseridas, atualizadas ou removidas.
	 */
	private DataModel<Entity> entities;

	/**
	 * Usado na manuten��o do resultado original da consulta feita, j� que
	 * a lista resultSearch poder� variar de acordo com a filtragem realizada
	 * pelo usu�rio.
	 */
	private List<Entity> originalResult;

	/**
	 * Servi�o usado para manipula��o dos dados enviados numa
	 * requisi��o do usu�rio.
	 */
	@ManagedProperty("#{genericService}")
	protected Service service;

	/**
	 * Inicializa as listas e entidades. Coloca no estado de consulta.
	 */
	public AbstractCrudBean() {
		resultSearch = new ListDataModel<Entity>();
		entities = new ListDataModel<Entity>();
		originalResult = new ArrayList<Entity>();
		entity = getNewEntityInstance();
		entitySearch = getNewEntityInstance();
		setCurrentState(STATE_SEARCH);
	}

	/**
	 * Cria uma lista de SelectItem com todos os dados da entidade ordenados
	 * pela descricao.
	 * 
	 * @return Lista de SelectItem.
	 */
	@SuppressWarnings("unchecked")
	public List<SelectItem> getAllSelectItem() {
		return FacesUtils.beanToSelectItem((List<Entidade>) getService()
				.findAll((Class<Entity>) entity.getClass(), "descricao"));
	}

	/**
	 * Seleciona um Entity entre o resultado da resultSearch
	 * 
	 * @param event
	 */
	public void selectEntity(ActionEvent event) {
		setSelectEntity((Entity) getResultSearch().getRowData());
	}

	/**
	 * Pega uma mensagem do properties.
	 * 
	 * @param key
	 * @return
	 */
	public String getMessage(String key) {
		if (messageProperties == null) {
			PropertiesUtils pUtils = new PropertiesUtils();
			messageProperties = pUtils.getProperties(getFileMessages());
		}
		return (String) getMessageProperties().get(key);
	}

	/**
	 * Manda salvar uma entidade.
	 * 
	 * @param event
	 */
	public void save(ActionEvent event) {
		try {
			executeSave();
		} catch (ApplicationException e) {
			addErrorMessage(e.getMessage());
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_INSERT"));
		}
	}

	public void saveAndEdit(ActionEvent event) {
		try {
			executeSaveAndEdit();
		} catch (ApplicationException e) {
			FacesUtils.addErrorMessage(e.getMessage());
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_INSERT"));
		}
	}

	/**
	 * Manda remover uma entidade, retira-a da lista de consulta e volta para o
	 * estado de consulta.
	 * 
	 * @param event
	 */
	public void delete(ActionEvent event) {
		try {
			executeDelete();
		} catch (ApplicationException e) {
			FacesUtils.addErrorMessage(e.getMessage());
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_REMOVE"));
		}
	}

	/**
	 * Manda atualizar uma entidade e volta para o estado de consulta.
	 * 
	 * @param event
	 */
	public void update(ActionEvent event) {
		try {
			executeUpdate();
		} catch (ApplicationException e) {
			FacesUtils.addErrorMessage(e.getMessage());
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_UPDATE"));
		}
	}

	/**
	 * Consulta uma entidade pela sua chave prim�ria.
	 * 
	 * @param event
	 */
	@SuppressWarnings("unchecked")
	public void findByPrimaryKey(ActionEvent event) {
		try {
			limpaListas();

			Entity res = service.findByPrimaryKey(
					(Class<Entity>) entitySearch.getClass(),
					ReflectionUtil.valueId(entitySearch));
			if (res != null) {
				getResultSearch().setWrappedData(new ArrayList<Entity>());
				((List<Entity>) getResultSearch().getWrappedData()).add(res);
			} else {
				FacesUtils.addWarnMessage(getMessage("NOT_FOUND"));
			}
			setCurrentState(STATE_SEARCH);
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_MESSAGE"));
		}
	}

	/**
	 * Consulta as entidade que possuem os par�metros informados, tais quais
	 * foram informados.
	 * 
	 * @param event
	 */
	@SuppressWarnings("unchecked")
	public void findByExample(ActionEvent event) {
		try {
			limpaListas();
			List<Entity> list = service.findByExample(entitySearch);
			if (!list.isEmpty()) {
				getResultSearch().setWrappedData(list);
				setOriginalResult((List<Entity>) getResultSearch()
						.getWrappedData());
			} else {
				FacesUtils.addWarnMessage(getMessage("NOT_FOUND"));
			}
			setCurrentState(STATE_SEARCH);
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_MESSAGE"));
		}
	}

	/**
	 * Consulta todas as entidades.
	 * 
	 * @param event
	 */
	@SuppressWarnings("unchecked")
	public void findAll(ActionEvent event) {
		try {
			limpaListas();
			List<Entity> list = service.findAll((Class<Entity>) entitySearch
					.getClass());
			if (!list.isEmpty()) {
				getResultSearch().setWrappedData(list);
				setOriginalResult((List<Entity>) getResultSearch()
						.getWrappedData());
			} else {
				FacesUtils.addWarnMessage(getMessage("NOT_FOUND"));
			}
			setCurrentState(STATE_SEARCH);
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_MESSAGE"));
		}
	}

	/**
	 * Consulta as entidades que satisfazem os par�metros informados em
	 * notEmptyFields do objeto.
	 * 
	 * @param event
	 */
	@SuppressWarnings("unchecked")
	public void findByFilter(ActionEvent event) {
		try {
			limpaListas();
			List<Entity> list = null;
			list = service.find(entitySearch, null,
					entitySearch.notEmptyFields());
			getResultSearch().setWrappedData(list);
			setOriginalResult((List<Entity>) getResultSearch().getWrappedData());
			setCurrentState(STATE_SEARCH);
		} catch (ApplicationException e) {
			FacesUtils.addWarnMessage(e.getMessage());
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_MESSAGE"));
		}
		// letraPesquisa = null;
	}

	/**
	 * Seleciona o tipo de consulta que ser� realizada, de acordo com os
	 * par�metros informados.
	 * 
	 * @param event
	 */
	public void find(ActionEvent event) {
		Map<String, Object> map = entitySearch.notEmptyFields();
		if (map == null || map.isEmpty()) {
			findAll(event);
		} else {
			if (ReflectionUtil.getNameFieldId(entitySearch) != null) {
				findByPrimaryKey(event);
			} else {
				findByFilter(event);
			}
		}
	}

	/**
	 * Salva todas as entidades que est�o na lista entities.
	 * 
	 * @param event
	 */
	@SuppressWarnings("unchecked")
	public void saveAll(ActionEvent event) {
		try {
			service.insertAll((List<Entity>) entities.getWrappedData());
			FacesUtils.addInfoMessage(getMessage("SUCCESS_ITENS_INSERT"));
			setCurrentState(STATE_EDIT);
			getEntities().setWrappedData(null);
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_MESSAGE"));
		}
	}

	/**
	 * Remove todas as entidades que est�o lista entities e atualiza a lista
	 * de resultados.
	 * 
	 * @param event
	 */
	@SuppressWarnings("unchecked")
	public void deleteAll(ActionEvent event) {
		try {
			if (getEntities().getRowCount() > 0) {
				service.removeAll((List<Entity>) entities.getWrappedData());

				// Remove os dados das lista de consultas.
				originalResult.removeAll((List<Entity>) entities
						.getWrappedData());
				((List<Entity>) resultSearch.getWrappedData())
						.removeAll((List<Entity>) entities.getWrappedData());

				FacesUtils.addInfoMessage(getMessage("SUCCESS_ITENS_REMOVE"));
				setCurrentState(STATE_SEARCH);
			}
		} catch (ApplicationException e) {
			FacesUtils.addErrorMessage(e.getMessage());
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_MESSAGE"));
		}
		getEntities().setWrappedData(null);
	}

	/**
	 * Atualiza todas as entidades que est�o na lista entities.
	 * 
	 * @param event
	 */
	@SuppressWarnings("unchecked")
	public void updateAll(ActionEvent event) {
		try {
			service.updateAll((List<Entity>) entities.getWrappedData());
			FacesUtils.addInfoMessage(getMessage("SUCCESS_ITENS_UPDATE"));
			setCurrentState(STATE_EDIT);
			getEntities().setWrappedData(null);
		} catch (Exception e) {
			FacesUtils.addErrorMessage(getMessage("ERROR_MESSAGE"));
		}
	}

	/**
	 * Filtra a lista de resultados da consulta pelo caractere inicial da
	 * descri��o.
	 * 
	 * @param event
	 */
	/*
	 * public void filterByLetter( ActionEvent event ){ List<Entity> list =
	 * getOriginalResult(); List<Entity> filtro = new ArrayList<Entity>(); if(
	 * letraPesquisa.equals("Todas") ){//Se tiver escolhido a op��o "Todas",
	 * coloca a lista da consulta original. getResultSearch().setWrappedData(
	 * getOriginalResult() ); }else{//Filtra. for( Entity e : list ){ if(
	 * e.getDescricao().toUpperCase().startsWith( letraPesquisa ) ){
	 * filtro.add(e); } } getResultSearch().setWrappedData( filtro ); } }
	 */

	/**
	 * Lista os caracteres alfanum�ricos (al�m da op��o 'Todas') usados
	 * para filtragem do resultado de uma consulta.
	 * 
	 * @return
	 */
	public List<String> getAlphaNumericList() {
		List<String> lista = new ArrayList<String>();
		lista.add("Todas");
		for (int i = 0; i < 10; i++) {// Algarismos de 0 a 9.
			lista.add(String.valueOf(i));
		}
		for (int i = 65; i <= 90; i++) {// Letras mai�sculas de A a Z.
			lista.add(String.valueOf((char) i));
		}
		return lista;
	}

	/**
	 * Inclue na lista entities a entidade selecionada num checkbox.
	 * 
	 * @param event
	 *            Evento disparado quando h� uma modifica��o no valor do
	 *            checkbox.
	 */
	@SuppressWarnings("unchecked")
	public void select(ValueChangeEvent event) {
		Entity e = (Entity) getResultSearch().getRowData();
		int ind = getResultSearch().getRowIndex();
		if (event.getNewValue().equals(true)) {
			if (getEntities().getWrappedData() == null) {
				getEntities().setWrappedData(new ArrayList<Entity>());
			}
			((List<Entity>) entities.getWrappedData()).add(e);
		} else if (entities.getRowCount() > 0 && ind < entities.getRowCount()) {
			((List<Entity>) entities.getWrappedData()).remove(ind);
		}
	}

	/**
	 * Lista as p�ginas do scoller de um dataTable.
	 * 
	 * @return
	 */
	/*
	 * public List<SelectItem> getPaginasScroller(){ List<SelectItem> lista =
	 * new ArrayList<SelectItem>();
	 * 
	 * for( int i = 1; i <= scroller.getPageCount(); i++ ){ lista.add( new
	 * SelectItem( i, String.valueOf(i) ) ); } return lista; }
	 */

	/**
	 * Retorna <code>true</code> se o estado estiver em edi��o.
	 * 
	 * @return
	 */
	public boolean isEditing() {
		return STATE_EDIT.equals(getCurrentState());
	}

	/**
	 * Retorna <code>true</code> se o estado estiver em inser��o.
	 * 
	 * @return
	 */
	public boolean isInserting() {
		return STATE_INSERT.equals(getCurrentState());
	}

	/**
	 * Retorna <code>true</code> se o estado estiver em exclus�o.
	 * 
	 * @return
	 */
	public boolean isDeleting() {
		return STATE_DELETE.equals(getCurrentState());
	}

	/**
	 * Retorna <code>true</code> se o estado estiver em visualiza��o.
	 * 
	 * @return
	 */
	public boolean isViewing() {
		return STATE_VIEW.equals(getCurrentState());
	}

	/**
	 * Retorna <code>true</code> se o estado estiver em atualiza��o, ou
	 * seja, caso o estado esteja em edi��o ou inser��o ou exclus�o.
	 * 
	 * @return
	 */
	public boolean isUpdating() {
		return (this.isEditing() || this.isInserting() || this.isDeleting());
	}

	/**
	 * Retorna <code>true</code> se o estado estiver em busca.
	 * 
	 * @return
	 */
	public boolean isSearching() {
		return (getCurrentState() == null || STATE_SEARCH
				.equals(getCurrentState()));
	}

	/**
	 * Prepara a tela para atualizar uma entidade.
	 * 
	 * @param event
	 */
	public void prepareUpdate(ActionEvent event) {
		// Alterna os panels necess�rios para mostrar o conte�do da
		// atualiza��o.
		entity = (Entity) getResultSearch().getRowData();
		setCurrentState(STATE_EDIT);
	}

	/**
	 * Prepara a tela para inserir uma entidade.
	 * 
	 * @param event
	 */
	public void prepareInsert(ActionEvent event) {
		// Alterna os panels necess�rios para mostrar o conte�do da
		// inser��o.
		setCurrentState(STATE_INSERT);
		limpaListas();
		setEntity(getNewEntityInstance());
	}

	/**
	 * Cancela a inclus�o / altera��o.
	 * 
	 * @param event
	 */
	public void cancel(ActionEvent event) {
		// Cancelar as opera��es, volta para a consulta
		init();
		FacesUtils.refresh();
	}

	/**
	 * Prepara a tela para excluir uma entidade.
	 * 
	 * @param event
	 */
	@SuppressWarnings("unchecked")
	public void prepareDelete(ActionEvent event) {
		// Alterna os panels necess�rios para mostrar o conte�do da
		// exclus�o.
		entity = (Entity) getResultSearch().getRowData();
		int ind = getResultSearch().getRowIndex();
		((List<Entity>) getResultSearch().getWrappedData()).remove(ind); // Remove
																			// o
																			// item
																			// a
																			// ser
																			// exclu�do
																			// da
																			// lista
																			// de
																			// consulta
		setCurrentState(STATE_DELETE);
	}

	/**
	 * Processa a inicializa��o dos dados
	 */
	public void init() {
		this.setCurrentState(STATE_SEARCH);
		this.numeroPagina = 1;
		limpaListas();
		this.setEntity(getNewEntityInstance());
		this.setEntitySearch(getNewEntityInstance());
		this.selectEntity(null);
	}

	/**
	 * Limpa as listas deste managed bean.
	 */
	protected void limpaListas() {
		this.getResultSearch().setWrappedData(null);
		getOriginalResult().clear();
		this.getEntities().setWrappedData(null);
	}

	/**
	 * Evento que dispara a inicializa��o. Pode ser usado por um
	 * ActionSource (bot�o, link, menu ou similar) para inicar a
	 * implementa��o de um caso de uso que fa�a parte da hierarquia dessa
	 * classe.
	 * 
	 * @param event
	 */
	public void init(ActionEvent event) {
		init();
	}

	@SuppressWarnings("unchecked")
	/**
	 * Instancia um novo objeto de acordo 
	 * com o subtipo da Entity.
	 */
	protected Entity getNewEntityInstance() {
		try {
			return (Entity) ((Class<Entity>) ((java.lang.reflect.ParameterizedType) this
					.getClass().getGenericSuperclass())
					.getActualTypeArguments()[0]).newInstance();
		} catch (Exception ex) {
			return null;
		}
	}

	/**
	 * @param Entity
	 *            the Entity to set
	 */
	public void setEntity(Entity entity) {
		this.entity = entity;
	}

	/**
	 * @return the Entity
	 */
	public Entity getEntity() {
		return entity;
	}

	/**
	 * @return the selectEntity
	 */
	public Entity getSelectEntity() {
		return selectEntity;
	}

	/**
	 * @param selectEntity
	 *            the selectEntity to set
	 */
	public void setSelectEntity(Entity selectEntity) {
		this.selectEntity = selectEntity;
	}

	/**
	 * 
	 * @param resultadoConsulta
	 */
	public void setResultSearch(DataModel<Entity> resultadoConsulta) {
		this.resultSearch = resultadoConsulta;
	}

	/**
	 * 
	 * @return
	 */
	public DataModel<Entity> getResultSearch() {
		return resultSearch;
	}

	/**
	 * 
	 * @param estadoCorrente
	 */
	public void setCurrentState(String currentState) {
		this.currentState = currentState;
	}

	public String getCurrentState() {
		return currentState;
	}

	/**
	 * Entity que parametriza a consulta
	 * 
	 * @param EntityConsulta
	 *            the EntityConsulta to set
	 */
	public void setEntitySearch(Entity entitySearch) {
		this.entitySearch = entitySearch;
	}

	/**
	 * @return the EntityConsulta
	 */
	public Entity getEntitySearch() {
		return entitySearch;
	}

	@SuppressWarnings("unused")
	private boolean mensagemErro;

	/**
	 * @return the mensagemErro
	 */
	public boolean isMensagemErro() {
		if (FacesUtils.isMessageErro()) {
			return true;
		}
		return false;

	}

	/**
	 * @param mensagemErro
	 *            the mensagemErro to set
	 */
	public void setMensagemErro(boolean mensagemErro) {
		this.mensagemErro = mensagemErro;
	}

	public Service getService() {
		return service;
	}

	public void setService(Service service) {
		this.service = service;
	}

	public DataModel<Entity> getEntities() {
		return entities;
	}

	public void setEntities(DataModel<Entity> entities) {
		this.entities = entities;
	}

	/*
	 * public String getLetraPesquisa() { return letraPesquisa; }
	 * 
	 * public void setLetraPesquisa(String letraPesquisa) { this.letraPesquisa =
	 * letraPesquisa; }
	 */

	public List<Entity> getOriginalResult() {
		return originalResult;
	}

	public void setOriginalResult(List<Entity> originalResult) {
		this.originalResult = originalResult;
	}

	public Integer getNumeroPagina() {
		return numeroPagina;
	}

	public void setNumeroPagina(Integer numeroPagina) {
		this.numeroPagina = numeroPagina;
	}

	/*
	 * public HtmlDatascroller getScroller() { return scroller; }
	 * 
	 * public void setScroller(HtmlDatascroller scroller) { this.scroller =
	 * scroller; }
	 */

	public String getFileMessages() {
		return fileMessages;
	}

	public void setFileMessages(String fileMessages) {
		this.fileMessages = fileMessages;
	}

	public Properties getMessageProperties() {
		return messageProperties;
	}

	public void setMessageProperties(Properties messageProperties) {
		this.messageProperties = messageProperties;
	}

	public void executeSave() {
		setEntity(service.insert(entity));
		FacesUtils.addInfoMessage(getMessage("SUCCESS_INSERT"));
		setCurrentState(STATE_INSERT);
		setEntity(getNewEntityInstance());
	}

	public void executeSaveAndEdit() {
		setEntity(service.insert(entity));
		FacesUtils.addInfoMessage(getMessage("SUCCESS_INSERT"));
		setCurrentState(STATE_EDIT);
	}

	public void executeDelete() {
		service.remove(entity);
		FacesUtils.addInfoMessage(getMessage("SUCCESS_REMOVE"));
		setCurrentState(STATE_SEARCH);
		setEntity(getNewEntityInstance());
	}

	public void executeUpdate() {
		service.update(entity);
		FacesUtils.addInfoMessage(getMessage("SUCCESS_UPDATE"));
		setCurrentState(STATE_SEARCH);
	}
}